package license;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.util.Date;
import java.util.Properties;

public class ElGamal {
	private int byte_strength = 31;
	private int byte_strength_key = byte_strength + 1;
	private int bit_strength = byte_strength_key*8;
	final static int PRIME_CERTAINTY = 128;
	private PublicKey public_key;
	private PrivateKey private_key;

	public ElGamal(int p_byte_strength_key){
		setSize(p_byte_strength_key);
	}

	public ElGamal(){
	}

	public void	setSize(int p_byte_strength_key){
		byte_strength = p_byte_strength_key-1;
		byte_strength_key = byte_strength + 1;
		bit_strength = byte_strength_key*8;
	}

	public void generateKeys() {
		int seed = (int)System.currentTimeMillis();
		BigInteger x;
		BigInteger y;
		BigInteger p;
		BigInteger g;

		// Pick p
		p = getPrime(seed, bit_strength, PRIME_CERTAINTY);
		// Pick g & x
		do g = random(seed, bit_strength); while (g.compareTo(p) >= 0);
		do x = random(seed, bit_strength); while (x.compareTo(p) >= 0 || x.compareTo(g) == 0);
		// Calculate y
		y = g.modPow(x, p);
		// Generate keys
		public_key = new PublicKey(y, p, g);
		private_key = new PrivateKey(x,p);
		//System.out.println("x=" + x + "\ny=" + y + "\ng=" + g + "\np=" + p);
	}

	private BigInteger getPrime(int seed, int size, int probability) {
		BigInteger p;
		BigInteger step = new BigInteger("2");
		for (p = random(seed, size);!p.isProbablePrime(probability);p = p.add(step));
		return p;
	}

	private static BigInteger random(int seed, int size) {
		if (size % 8 != 0) {
			size = size + (8 - size % 8);
		}
		byte[] bytes = new byte[size / 8];
		BigInteger ir = new BigInteger("" + 0x7fffffff);
		BigInteger half_ir = new BigInteger("" + 0x3fffffff);
		// Fix seed
		if (seed <= 0) seed = (int)(Math.random() * 0x7fffffff);
		while (seed <= 65536) seed *= 2;
		// Add bits
		for (int t = 0;t < size / 8;t++) {
			bytes[t] = 0;
		}
		for (int t = 0;t < size;t++) {
			BigInteger r =
				new BigInteger("" + (int)(Math.random() * 0x7fffffff))
				.add(new BigInteger("" + seed))
				.mod(ir);
			if (r.compareTo(half_ir) < 0 || t == 0 || t == (size - 1)) {
				bytes[t / 8] |= (1 << (7 - (t % 8)));
			}
		}
		return new BigInteger(1, bytes);
	}

	public PublicKey getPublicKey() {
		return public_key;
	}

	public PrivateKey getPrivateKey() {
		return private_key;
	}

	public void setPublicKey(PublicKey _public_key) {
		public_key = _public_key;
	}

	public void setPrivateKey(PrivateKey _private_key) {
		if (_private_key == null) {
			System.out.println("Private Key is null");
		} else {
			System.out.println("Private Key is not null");
		}
		private_key = _private_key;
	}

	public static class PublicKey {
		private BigInteger y;
		private BigInteger p;
		private BigInteger g;

		public PublicKey(BigInteger _y, BigInteger _p, BigInteger _g) {
			y = _y;
			p = _p;
			g = _g;
		}

		public PublicKey(Properties props) {
			String sp=(String)props.get("p");
			String sy=(String)props.get("y");
			String sg=(String)props.get("g");
			if (sp==null) sp=(String)props.get("P");
			if (sy==null) sy=(String)props.get("Y");
			if (sg==null) sg=(String)props.get("G");
			p = new BigInteger(sp);
			y = new BigInteger(sy);
			g = new BigInteger(sg);
		}

		public BigInteger getY() {
			return y;
		}

		public BigInteger getP() {
			return p;
		}

		public BigInteger getG() {
			return g;
		}

		public Properties getProperties(){
			Properties props=new Properties();
			props.setProperty("y",y.toString());
			props.setProperty("p",p.toString());
			props.setProperty("g",g.toString());
			return props;
		}
	}

	public static class PrivateKey {
		private BigInteger x;
		private BigInteger p;

		public PrivateKey(BigInteger _x,BigInteger _p) {
			x = _x;
			p = _p;
		}

		public PrivateKey(Properties props) {
			String sx=(String)props.get("x");
			String sp=(String)props.get("p");
			if (sx==null) sx=(String)props.get("X");
			if (sp==null) sp=(String)props.get("P");
			x = new BigInteger(sx);
			p = new BigInteger(sp);
		}

		public BigInteger getX() {
			return x;
		}
		public BigInteger getP() {
			return p;
		}

		public Properties getProperties(){
			Properties props=new Properties();
			props.setProperty("x",x.toString());
			props.setProperty("p",p.toString());
			return props;
		}
	}

	public byte[] encrypt(byte[] buffer) {
		int size = buffer.length;
		int padding = 0;
		BigInteger g = public_key.getG();
		BigInteger y = public_key.getY();
		BigInteger p = public_key.getP();
		BigInteger k;
		BigInteger one = new BigInteger("1");
		BigInteger p_minus_one = p.subtract(one);

		// Round buffer size up
		if (size % byte_strength != 0) {
			padding = (size % (byte_strength));
			size += byte_strength - padding;
		}
		byte[] newbuffer = new byte[size / byte_strength * byte_strength_key * 2];
		byte[] single = new byte[byte_strength];
		// Break into parts and encrypt each
		for (int t = 0;t < size / byte_strength;t++) {
			do k = random((int)System.currentTimeMillis(), bit_strength); while (k.compareTo(p) >= 0 || !k.gcd(p_minus_one).equals(one));
			// Copy and padd into single
			for (int c = 0;c < byte_strength;c++) {
				if (t*byte_strength + c < buffer.length) {
					single[c] = buffer[t * byte_strength + c];
				} else {
					single[c] = (byte)(Math.random() * 255);
				}
			}
			// Do maths
			BigInteger m = new BigInteger(1, single);
			BigInteger a = g.modPow(k, p);
			BigInteger b = y.modPow(k, p).multiply(m).mod(p);
			// Write into new buffer
			byte[] ab = a.toByteArray(), bb = b.toByteArray();
			byteArrayCopy(newbuffer, ab, ab.length - byte_strength_key , t*2*byte_strength_key, byte_strength_key);
			byteArrayCopy(newbuffer, bb, bb.length - byte_strength_key , t*2*byte_strength_key + byte_strength_key, byte_strength_key);
		}
		return newbuffer;
	}

	public byte[] sign(byte[] buffer) {
		int size = buffer.length;
		int padding = 0;
		BigInteger g = public_key.getG();
		BigInteger y = public_key.getY();
		BigInteger x = private_key.getX();
		BigInteger p = private_key.getP();
		BigInteger k;
		BigInteger one = new BigInteger("1");
		BigInteger p_minus_one = p.subtract(one);

		// Round buffer size up
		if (size % byte_strength != 0) {
			padding = (size % (byte_strength));
			size += byte_strength - padding;
		}
		byte[] newbuffer = new byte[size / byte_strength * byte_strength_key * 2];
		byte[] single = new byte[byte_strength_key+1];
		// Break into parts and encrypt each
		for (int t = 0;t < size / byte_strength;t++) {
			do k = random((int)System.currentTimeMillis(), bit_strength); while (k.compareTo(p) >= 0 || !k.gcd(p_minus_one).equals(one));
//			// Copy and padd into single
//			for (int c = 0;c < byte_strength;c++) {
//				if (t*byte_strength + c < buffer.length) {
//					single[c] = buffer[t * byte_strength + c];
//				} else {
//					single[c] = 0; // Pad only with zeros
//				}
//			}
			byteArrayCopy(single, buffer, t*byte_strength, 1, byte_strength_key);
			// Do maths
			BigInteger m = new BigInteger(1, single);
			BigInteger a = g.modPow(k, p);
			BigInteger b = k.modInverse(p_minus_one).multiply(m.subtract(x.multiply(a)).mod(p_minus_one)).mod(p_minus_one);

			// Write into new buffer
			byte[] ab = a.toByteArray(), bb = b.toByteArray();
			byteArrayCopy(newbuffer, ab, ab.length - byte_strength_key , t*2*byte_strength_key, byte_strength_key);
			byteArrayCopy(newbuffer, bb, bb.length - byte_strength_key , t*2*byte_strength_key + byte_strength_key, byte_strength_key);
		}
		return newbuffer;
	}

	private void byteArrayCopy(byte[] dest, byte[] source, int from_index , int to_index, int bytes) {
		// Right alight bytes
		if (from_index < 0) from_index = 0;
		int start = 0;
		int remLength=source.length-from_index;
		if (remLength < bytes) {
			start = bytes - remLength;
			for (int t = 0;t < start;t++) {
				dest[t + to_index] = 0;
			}
		}
		for (int t = start;t < bytes;t++) {
			dest[t + to_index] = source[t + from_index - start];
		}
	}

	public byte[] decrypt(byte[] buffer, int origanal_size) {
		if (private_key == null) {
			System.out.println("Private key i snull");
		}
		byte[] newbuffer = new byte[origanal_size];
		byte[] single = new byte[byte_strength_key + 1];
		byte[] m_bytes = new byte[byte_strength];
		BigInteger x = private_key.getX();
		BigInteger p = private_key.getP();
		int bytes_to_write = byte_strength;
		// Do process
		for (int t = 0;t < buffer.length / byte_strength_key / 2;t++) {
			BigInteger m;
			byteArrayCopy(single, buffer, t*byte_strength_key*2, 1, byte_strength_key);
			BigInteger a = new BigInteger(1, single);
			byteArrayCopy(single, buffer, t*byte_strength_key*2 + byte_strength_key, 1, byte_strength_key);
			BigInteger b = new BigInteger(1, single);

			// Do calculation
			m = b.multiply(a.modInverse(p).modPow(x, p)).mod(p);
			//System.out.println("a="+a+ " b= "+b+" M="+m);

			if (t*byte_strength + bytes_to_write > origanal_size) {
				bytes_to_write = origanal_size - t * (byte_strength);
			}
			// Decrypted enough?
			if (t*byte_strength > origanal_size) {
				break;
			}
			byte[] mb = m.toByteArray();
			byteArrayCopy(m_bytes, mb, mb.length - byte_strength , 0, byte_strength);
			byteArrayCopy(newbuffer, m_bytes, 0, t*byte_strength, bytes_to_write);
		}
		return newbuffer;
	}

	public boolean verify(byte[] buffer,byte[] origanal) {
		byte[] single = new byte[byte_strength_key + 1];
		byte[] m_bytes = new byte[byte_strength];
		BigInteger g = public_key.getG();
		BigInteger y = public_key.getY();
		BigInteger p = public_key.getP();
		int bytes_to_write = byte_strength;
		boolean isok=((buffer.length/byte_strength_key/2)>0);
		// Do process
		for (int t = 0;t < buffer.length / byte_strength_key / 2 && isok;t++) {
			byteArrayCopy(single, origanal, t*byte_strength, 1, byte_strength_key);
			BigInteger m = new BigInteger(1, single);
			byteArrayCopy(single, buffer, t*byte_strength_key*2, 1, byte_strength_key);
			BigInteger a = new BigInteger(1, single);
			byteArrayCopy(single, buffer, t*byte_strength_key*2 + byte_strength_key, 1, byte_strength_key);
			BigInteger b = new BigInteger(1, single);

			//System.out.println("a="+a+ " b= "+b+" M="+m);
			isok=y.modPow(a, p).multiply(a.modPow(b, p)).mod(p).equals(g.modPow(m, p));
		}
		return isok;
	}

/*	public static void main(String args[]) {
		// Tets random
		ElGamal elgamal = new ElGamal();
		elgamal.generateKeys();
		System.out.println("Started");
		for (int i = 0;i < 1000;i++) {
		byte cipher [] = elgamal.encrypt("Do This encryption".getBytes());
		}
		System.out.println("Done");
	}
*/
	public static void main(String[] args) {
		ElGamal e = new ElGamal(64);
		e.generateKeys();
		String filename=args[0];
		Date start=null,middle=null,end=null;
		try {
			int org_size;
			{
				start=new Date();
				FileInputStream fi = new FileInputStream(filename);
				FileOutputStream fe = new FileOutputStream(filename+".encrypted");
				FileOutputStream fs = new FileOutputStream(filename+".signed");
				byte[] stuff = new byte[e.byte_strength];
				int pos=0;
				int fsize=fi.available();
				org_size=fsize;
				System.out.print("Encrypt ");
				int last_p=0;
				while (fi.available() > 0) {
					int size = fi.read(stuff);
					pos+=size;
					if (last_p!=pos*100/fsize){
						last_p=pos*100/fsize;
						System.out.print(".");
					}
					byte[] encrypted = e.encrypt(stuff);
					fe.write(encrypted);

					// Test signing too
					byte[] sign = e.sign(stuff);
					fs.write(sign);
				}
				fi.close();
				fe.close();
				fs.close();
			}

			{
				middle=new Date();
				FileInputStream fe = new FileInputStream(filename+".encrypted");
				FileInputStream fs = new FileInputStream(filename+".signed");
				FileOutputStream fd = new FileOutputStream(filename+".decrypted");
				byte[] stuff = new byte[e.byte_strength_key * 2];
				int fsize=fe.available();
				int pos=0;
				int write=e.byte_strength;
				int written=0;
				System.out.println("");
				System.out.print("Decrypt ");
				int last_p=0;
				while (fe.available() > 0) {
					int size = fe.read(stuff);
					pos+=size;
					if (last_p!=pos*100/fsize){
						last_p=pos*100/fsize;
						System.out.print(".");
					}
					byte[] decrypted = e.decrypt(stuff,e.byte_strength);
					if (write+written>org_size){
						write=org_size-written;
					}
					fd.write(decrypted,0,write);
					written+=write;

					// Test signed
					fs.read(stuff);
					if (!e.verify(stuff,decrypted)){
						System.out.println("");
						System.out.println("Fail on verify.");
						System.exit(-1);
					}
				}
				System.out.println("");
				fe.close();
				fd.close();
				end=new Date();
			}
		} catch (IOException ioe) {
			ioe.printStackTrace();
		}
		System.out.println("Start="+start);
		System.out.println("Middle="+middle);
		System.out.println("End="+end);
	}
	// java -cp "com.applications.security.jar" com.applications.security.ElGamal
}

